package com.livestocksolutions;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;

import com.livestocksolutions.interfaces.Observable;
import com.livestocksolutions.util.UpdateHistory;
/*
 *  1.0 : ? 2012 - Initial Version
 * 
 *  1.1 : 07 October 2012 - Moved Joining apply methods and associated data to Group class
 *                 so that a Joining can also be made to a group of Cattle too.
 *                 Modified #applyFeedToGroup(FeedHistory) and #applyMedicineToGroup(MedicineHistory)
 *                 to add individual histories to members of the Group and not the same history to all.
 */

/**
 * Group class represents a group of cattle (bovine object)
 * This class also provides the base for the Mob class
 *
 * @author Matt Lang
 * @author Adam Parr
 * 
 * @version 1.1
 * @.modified 18 March 2013
 */
public class Group implements Observable{
	/**
	 * Unique identification that will identify this Group
	 */
	protected final long id;
	/**
	 * Keep track of any changes of this Group.
	 */
	protected HashMap<String,UpdateHistory> update = new HashMap<String,UpdateHistory>();
	
	/**
	 * Any joinings applied to this group
	 */
	private ArrayList<Long> currentJoinings = new ArrayList<Long>();
	/**
	 * Attribute designates the name to the created group.
	 */
	protected String name;
	/**
	 * Description about the group.
	 */
	protected String description;
	/**
	 * Arraylist contains the current members for the group.
	 */
	protected ArrayList<Long> members = new ArrayList<Long>();
	/**
	 * Arraylist of observations about the group.
	 */
	protected ArrayList<Long> observations = new ArrayList<Long>();
	
	/**
	 *constructor to setup the class giving the name as the inital value
	 *@param name initial setup value for a group
	 */
	public Group(String name)
	{
		//setup initial variable name
		this.name=name;
		
		long tempID = Calendar.getInstance().getTimeInMillis();
		while(LivestockSolutions.getGroupByID(tempID)!=null){
			// Just in case multiple items of this type are made in the same millisecond
			tempID++;
		}
		id=tempID;
		
		update.put("ID", new UpdateHistory(String.valueOf(id)));
		update.put("NAME", new UpdateHistory(this.name));
	}
	
	
	/**
	 * Get the internal ID of this Group Object
	 * @return An unique long value representing this Object.
	 */
	public long getID(){
		return id;
	}
	
	/**
	 *method to get the name of the group
	 *@return String containing the name
	 */
	public String getName()
	{
		return name;
	}
	
	/**
	 *method to get the description of the group
	 *@return String containing the description
	 */
	public String getDescription()
	{
		return description;
	}
	
	/**
	 *method to get the cattle in the group
	 *@return an array of bovine objects currently in the group
	 */
	public Bovine[] getBovineList()
	{
		int size;
		Bovine[] me = new Bovine[size = members.size()];
		for(int x=0; x<size; x++){
			me[x]=LivestockSolutions.getBovineByID(members.get(x));
		}
		return me;
	}
	
	/**
	 *method to get the observations about a certain group
	 *@return an array of observation objects.
	 */
	@Override
	public Observation[] getObservations(){
		int size;
		Observation[] ob = new Observation[size = observations.size()];
		for (int x=0; x<size; x++) {
			ob[x] = LivestockSolutions.getObservationByID(observations.get(x));
		}
		return ob;
	}
	
	@Override
	public boolean removeObservation(Observation observation) {
		long oi=observation.getID();
		boolean value=observations.remove(oi);
		update.get("OBSERVATION").getValue(String.valueOf(observation.getID())).setValue(String.valueOf(!value));
		return value;
	}
	
	/**
	 *method sets the name of the group
	 *@param name new name to be written to the group
	 */
	public void setName(String name) {
		this.name=name;
		update.put("NAME", new UpdateHistory(this.name));
	}
	
	/**
	 *method sets the description of the group
	 *@param description new description to be written to the group
	 */
	public void setDescription(String description)
	{
		this.description = description;
		update.put("DESCRIPTION", new UpdateHistory(this.description));
	}
	/**
	 *method adds a bovine to the group.
	 * @param bovine The bovine to add.
	 *@return boolean true if successful and false if unsuccessful.
	 */
	public boolean addBovine(Bovine bovine)
	{
		//checks that bovine doesn't already exist in the array
		if(!members.contains(bovine.getID()))
		{
			//adds the bovine to the group
			members.add(bovine.getID());
			
			UpdateHistory memberList = update.get("MEMBER")!=null?update.get("MEMBER"):new UpdateHistory();
			memberList.setValues(String.valueOf(bovine.getID()),new UpdateHistory("true"));
			update.put("MEMBER", memberList);
			//returns true to indicate bovine was added
			return true;
		}
		//returns false to indicate bovine was not added
		return false;
	}//end addBovine
	
	/**
	 *Method to add an observation about a group.
	 *@param observation observation about the group to be added
	 */
	public void addObservation(Observation observation){
		if(!observations.contains(observation.getID())){
			observations.add(observation.getID());
			UpdateHistory observationList = update.get("OBSERVATION")!=null? update.get("OBSERVATION"):new UpdateHistory();
			observationList.setValues(String.valueOf(observation.getID()),new UpdateHistory("true"));
			update.put("OBSERVATION", observationList);
		}
		observation.setOwner(this);
		LivestockSolutions.addObservation(observation);
	}//end addObservation
	
	/**
	 *Method removes a bovine from the group
	 *@param bovine to be removed
	 * @return <tt>true</tt> if bovine exists and was removed,
	 * <tt>false</tt> otherwise
	 */
	public boolean removeBovine(Bovine bovine)
	{
		//checks that the bovine to be removed exists in the array
		if(members.contains(bovine.getID()))
		{
			//removes bovine and returns true to indicate that.
			members.remove(bovine.getID());
			
			UpdateHistory memberList = update.get("MEMBER")!=null?update.get("MEMBER"):new UpdateHistory();
			memberList.setValues(String.valueOf(bovine.getID()),new UpdateHistory("false"));
			update.put("MEMBER", memberList);
			return true;
		}
		//returns false to indicate bovine was not removed i.e didnt exist in the array.
		return false;
	}//end removeBovine
	
	/**
	 * Get the current joinings applied to this mob
	 * @return an Array of Join that are current for animals within this mob
	 */
	public Join[] getCurrentJoinings(){
		currentJoinings=currentJoinings==null?new ArrayList<Long>():currentJoinings;
		Iterator<Long> i = currentJoinings.iterator();
		while(i.hasNext()){
			Join j = LivestockSolutions.getJoiningByID(i.next());
			Calendar c = Calendar.getInstance();
			Bovine b = j.getBull();
			// Gestation period plus 60 days after bull is removed will be when this Joining is considered inactive
			c.add(Calendar.DATE, b.getBreed().getGestationPeriodInDays()+60);
			if(j.getRemovalDate()!=null && j.getRemovalDate().after(c.getTime())){
				i.remove();
			}
		}
		int size = currentJoinings.size();
		Join[] jo = new Join[size];
		for(int x=0; x<size; x++){
			jo[x]=LivestockSolutions.getJoiningByID(currentJoinings.get(x));
		}
		return jo;
	}
	
	/**
	 * Add a new Joining to this mob
	 * @param joining the Join to add
	 */
	public void applyJoiningToMob(Join joining){
		currentJoinings.add(joining.getID());
		
		UpdateHistory joiningList = update.get("JOINING")!=null? update.get("JOINING"):new UpdateHistory();
		joiningList.setValues(String.valueOf(joining.getID()),new UpdateHistory("true"));
		update.put("JOINING", joiningList);
		
		for(Bovine b : getBovineList()){
			if(b.getGender().equalsIgnoreCase(Cow.GENDER)){
				//TODO check that this Cow is old enough to be added.
				joining.addCow((Cow)b);
			}
		}
	}
	
	/**
	 *Method applies feed to a group of Bovine
	 *@param feed is a type of feed to be applied to the bovine within the group
	 */
	public void applyFeedToGroup(FeedHistory feed)
	{
		//for each loop adds feed to each individual bovine
		for (long x: members) {
			Bovine b = LivestockSolutions.getBovineByID(x);
			if(b!=null){
				b.addHandFeed(feed);
			}
		}
	}//end applyFeedToGroup
	
	/**
	 *Method applies a medicine to the bovine within the group
	 *@param medicine to be applied to all bovine in the group.
	 */
	public void applyMedicineToGroup(MedicineHistory medicine){		
		//for each loop to individually add the medicine to each bovine in the array
		for (long x: members) {
			Bovine b = LivestockSolutions.getBovineByID(x);
			if(b!=null){
				b.addMedicine(medicine);
			}
		}
	}//end applyMedicineToGroup
	
	/**
	 * Updates this Object with the updated values from the different version.
	 * @param changes Group that represents this Object in a different version.
	 * @return <tt>true</tt> if changes is not null and changes does not equal this object, <tt>false</tt> otherwise.
	 */
	public boolean update(Group changes){
		if(changes!=null && this!=changes){
			if(updateBefore("NAME",changes)){
				name=changes.update.get("NAME").getValue();
				update.put("NAME", new UpdateHistory(name));
			}
			
			if(updateBefore("DESCRIPTION",changes)){
				description=changes.update.get("DESCRIPTION").getValue();
				update.put("DESCRIPTION", new UpdateHistory(description));
			}
			
			// Associative ArrayLists (Only Long ID)
			// Add Observations that may be new
			if(changes.update.get("OBSERVATION")!=null){
				for(String key : changes.update.get("OBSERVATION").toKeyArray()){
					if(update.get("OBSERVATION")==null || update.get("OBSERVATION").getValue(key)==null || !update.get("OBSERVATION").getValue(key).getDate().after(changes.update.get("OBSERVATION").getValue(key).getDate())){
						UpdateHistory observationList = update.get("OBSERVATION")!=null?update.get("OBSERVATION"):new UpdateHistory();
						if(changes.update.get("OBSERVATION").getValue(key).equals("false")){
							// Remove
							observations.remove(Long.parseLong(key));
							observationList.setValues(key,new UpdateHistory("false"));
						}else{
							// Add
							if(!observations.contains(Long.parseLong(key))){
								observations.add(Long.parseLong(key));
							}
							observationList.setValues(key,new UpdateHistory("true"));
						}
						update.put("OBSERVATION", observationList);
					}
				}
			}
			
			// Add Member that may be new
			if(changes.update.get("MEMBER")!=null){
				for(String key : changes.update.get("MEMBER").toKeyArray()){
					if(update.get("MEMBER")==null || update.get("MEMBER").getValue(key)==null || !update.get("MEMBER").getValue(key).getDate().after(changes.update.get("MEMBER").getValue(key).getDate())){
						UpdateHistory memberList = update.get("MEMBER")!=null?update.get("MEMBER"):new UpdateHistory();
						if(changes.update.get("MEMBER").getValue(key).equals("false")){
							// Remove
							members.remove(Long.parseLong(key));
							memberList.setValues(key,new UpdateHistory("false"));
						}else{
							// Add
							if(!members.contains(Long.parseLong(key))){
								members.add(Long.parseLong(key));
							}
							memberList.setValues(key,new UpdateHistory("true"));
						}
						update.put("MEMBER", memberList);
					}
				}
			}
			
			// Add Joining that may be new
			if(changes.update.get("JOINING")!=null){
				for(String key : changes.update.get("JOINING").toKeyArray()){
					if(update.get("JOINING")==null || update.get("JOINING").getValue(key)==null || !update.get("JOINING").getValue(key).getDate().after(changes.update.get("JOINING").getValue(key).getDate())){
						UpdateHistory joiningList = update.get("JOINING")!=null?update.get("JOINING"):new UpdateHistory();
						if(!changes.update.get("JOINING").getValue(key).equals("false")){
							// Remove
							currentJoinings.remove(Long.parseLong(key));
							joiningList.setValues(key,new UpdateHistory("false"));
						}else{
							// Add
							if(!currentJoinings.contains(Long.parseLong(key))){
								currentJoinings.add(Long.parseLong(key));
							}
							joiningList.setValues(key,new UpdateHistory("true"));
						}
						update.put("OFFSPRING", joiningList);
					}
				}
			}
			
			// Check if any Joining is not current
			// TODO Check if any Joining is not current.
			return true;
		}
		return false;
	}
	
	/**
	 * Checks to see if the modification date of the item at this position is before the changed value
	 * @param key The String key at which to compare the modification dates of the modified objects.
	 * @param changes The Group that represents this Group on another system.
	 * @return <tt>true</tt> if the old value was modified before the changed value. <tt>false</tt> otherwise.
	 */
	protected boolean updateBefore(String key,Group changes){
		if(this.update.get(key)==null && changes.update.get(key)!=null){
			return true;
		}else if(changes.update.get(key)==null){
			return false;
		}
		return !this.update.get(key).getDate().after(changes.update.get(key).getDate());
	}
}//end class
